Оглавление :
Шаг 1. Загрузка данных и подготовка их к анализу
1.2 Источники пользователей 1.3 Пользовательские действия в игре
2.1 Количество пользователей в день/неделю/месяц
2.3 Какие стратегии используются для завершения уровня
2.5 Сколько времени занимает прохождение всего уровня для разных стратегий
3.1 Сколько в целом потратили на разные источники/в день
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
import datetime as dt
from scipy import stats as st
from plotly import graph_objects as go
import plotly.express as px
from plotly.subplots import make_subplots
ad_costs = pd.read_csv('datasets/ad_costs.csv')
ad_costs
ad_costs.info()
Пропусков в данных нет, нужно изменить тип данных для дат:
ad_costs['day'] = ad_costs['day'].map(lambda x: dt.datetime.strptime(x, '%Y-%m-%d'))
user_source = pd.read_csv('datasets/user_source.csv')
user_source
user_source.info()
game_actions = pd.read_csv('datasets/game_actions.csv')
game_actions
game_actions.info()
Проверим пропуски в данных и изменим тип данных для дат:
game_actions.isnull().sum()/game_actions.shape[0]*100
game_actions.loc[game_actions['building_type'].isnull()]
Эти строки — пользователи, которые выбрали боевую стратегию и закончили уровень победой над врагом.
game_actions['project_type'].unique()
game_actions['event'].unique()
game_actions['event_datetime'] = pd.to_datetime(game_actions['event_datetime'], format='%Y-%m-%d')
game_actions
Загрузили и обработали данные для дальнейшего анализа.
game_actions['session_year'] = game_actions['event_datetime'].dt.year
game_actions['session_month'] = game_actions['event_datetime'].astype('datetime64[M]')
game_actions['number_session_month'] = game_actions['event_datetime'].dt.month
game_actions['session_week'] = game_actions['event_datetime'].astype('datetime64[W]')
game_actions['number_session_week'] = game_actions['event_datetime'].dt.week
game_actions['session_date'] = game_actions['event_datetime'].dt.date
dau = game_actions.groupby('session_date').agg({'user_id': 'nunique'})
wau = game_actions.groupby(['session_year', 'number_session_week']).agg({'user_id': 'nunique'})
mau = game_actions.groupby(['session_year', 'number_session_month']).agg({'user_id': 'nunique'})
dau_total = dau.mean()
wau_total = wau.mean()
mau_total = mau.mean()
print('Уникальных пользователей в день:', int(dau_total))
print('Уникальных пользователей в неделю:', int(wau_total))
print('Уникальных пользователей в месяц:', int(mau_total))
dau.plot(grid=True, color = '#a52a2a',
figsize=(16,8), label='Количество пользователей',
legend=True, title='Количество уникальных пользователей в день');
Количество уникальных пользователей росло до 10 мая, а затем начало сокращаться.
wau.plot(kind='bar', grid=True, color = '#a52a2a',
figsize=(16,8), label='Количество пользователей',
legend=True, title='Количество уникальных пользователей в неделю');
В первые 2 недели количество уникальных пользователей было более 12000 человек, а затем сократилось более, чем в 2 раза.
mau.plot(kind='bar', grid=True, color = '#a52a2a',
figsize=(16,8), label='Количество пользователей',
legend=True, title='Количество уникальных пользователей в месяц');
У нас практически нет данных за июнь, поэтому сравнить количество уникальных пользователей в месяц не выйдет.
count_sessions = game_actions.groupby('session_date').agg({'user_id': 'count'})
count_sessions.plot(grid=True, color = '#a52a2a',
figsize=(16,8), label='Количество пользователей', legend=True, title='Количество сессий в день');
Как и количество уникальных пользователей, количество сессий росло до 10 мая, а затем начало сильно снижаться.
count_sessions_mean = count_sessions.mean()
print('Количество сессий в день:', int(count_sessions_mean))
event_users = (game_actions
.groupby('event')
.agg({'user_id':['count', 'nunique']})
.sort_values(by=('user_id', 'count'), ascending=False)
.reset_index())
event_users.columns = ['event', 'total_events', 'total_users']
event_users
За указанный период 13576 пользователей успели построить 127957 объектов. 5817 пользователей закончили первый уровень, из них 1866 завершили проект.
building_users = (game_actions
.groupby('building_type')
.agg({'user_id':['count', 'nunique']})
.sort_values(by=('user_id', 'count'), ascending=False)
.reset_index())
building_users.columns = ['building_type', 'total_buildings', 'total_users']
building_users
Объект spaceport был построен наибольшее количество раз (59325), assembly_shop был построен 54494 раз, а research_center построили только 14138 раз.
game_actions
building_type = game_actions.groupby(['session_date', 'building_type'])['user_id'].count().reset_index()
building_type
fig = px.line(building_type, x='session_date', y='user_id', color='building_type')
fig.update_layout(
title='Распределение построек по времени',
title_x = 0.5,
margin=dict(l=50, r=50, t=130, b=50))
fig.show()
Первая постройка – assembly_shop. Вероятно, этот объект строится очень быстро и первый в очереди построек. Его построили максимальное количество пользователей, затем строились только другие объеты. Возможно, для первого уровня можно построить только 1 такой объект.
Объект research_center начали строить с задержкой, вероятно, он идёт последним для завершения уровня. Таким образом можно установить порядок строительства объектов: assembly_shop -> spaceport -> research_center
Также можно отметить, что с 13 мая активность построек упала по всем объектам: наверное, какие-то пользователи завершили уровень, а какие-то забросили игру.
Проверим, сколько времени пользователь провел в приложении:
first_visits = game_actions.groupby(['user_id'])['event_datetime'].min().reset_index()
first_visits.columns = ['user_id', 'first_visit']
last_visits = game_actions.groupby(['user_id'])['event_datetime'].max().reset_index()
last_visits.columns = ['user_id', 'last_visit']
duration_using = first_visits.merge(last_visits, on='user_id')
duration_using
duration_using['duration_days'] = (duration_using['last_visit'] - duration_using['first_visit']).dt.days
duration_using
print('Среднее количество дней в игре:', int(duration_using['duration_days'].mean()))
fig = px.histogram(duration_using, x='duration_days')
fig.update_layout(
barmode='stack',
title='Количество дней, проведённых пользователями в игре',
title_x = 0.5,
margin=dict(l=50, r=50, t=130, b=50))
fig.show()
В среднем пользователь проводит в игре 10 дней.
Теперь проверим, сколько времени пользователь тратит на построение объекта/завершение уровня:
events = game_actions[['user_id', 'event_datetime', 'event']]
users_strategy = game_actions[['user_id', 'project_type']].query('project_type == "satellite_orbital_assembly"')
duration_events = duration_using.merge(events, left_on=['user_id', 'last_visit'], right_on=['user_id', 'event_datetime'])
duration_events = duration_events.merge(users_strategy, how='left', on='user_id')
duration_events
Пометим пользователей, которые выбрали боевую стратегию и закончили уровень победой над врагом, как 'fight'
duration_events['project_type'] = duration_events['project_type'].fillna('fight')
users_with_build_strategy = duration_events.query('project_type == "satellite_orbital_assembly"')
users_with_fight_strategy = duration_events.query('event == "finished_stage_1" and project_type == "fight"')
fig = go.Figure()
fig.add_trace(go.Histogram(x=users_with_build_strategy['duration_days'], name='build_strategy'))
fig.add_trace(go.Histogram(x=users_with_fight_strategy['duration_days'], name='fight_strategy'))
fig.update_layout(
barmode='stack',
title='Количество дней для завершения проекта',
title_x = 0.5,
margin=dict(l=50, r=50, t=130, b=50))
fig.update_traces(opacity=0.75)
fig.show()
Пользователей, выбравших стратегию строительства, меньше и они в среднем тратят больше времени для прохождения уровня.
users_not_finished = duration_events.query('event == "building"')
fig = px.histogram(users_not_finished, x='duration_days')
fig.update_layout(
barmode='stack',
title='Количество дней в игре без завершения уровня',
title_x = 0.5,
margin=dict(l=50, r=50, t=130, b=50))
fig.show()
Пользователи, которые не завершили уровень, тратят в среднем 9 дней в игре.
build_strategy = (users_with_build_strategy
.pivot_table(index='event', values=['user_id', 'duration_days'], aggfunc={'user_id':'count', 'duration_days':'mean'})
.rename({'finished_stage_1':'users_with_build_strategy'}, axis=0))
build_strategy
fight_strategy = (users_with_fight_strategy
.pivot_table(index='event', values=['user_id', 'duration_days'], aggfunc={'user_id':'count', 'duration_days':'mean'})
.rename({'finished_stage_1':'users_with_fight_strategy'}, axis=0))
without_strategy = (users_not_finished
.pivot_table(index='event', values=['user_id', 'duration_days'], aggfunc={'user_id':'count', 'duration_days':'mean'})
.rename({'building':'users_not_finished'}, axis=0))
all_strategies = pd.concat([build_strategy, fight_strategy, without_strategy])
all_strategies
all_strategies['%_of_total'] = (all_strategies['user_id'] / all_strategies['user_id'].sum())*100
all_strategies
fig = go.Figure()
fig.add_trace(go.Pie(values=all_strategies['%_of_total'], labels=all_strategies.index))
fig.update_layout(
title='Статистика выбора стратегий',
title_x = 0.5,
margin=dict(l=50, r=50, t=130, b=50))
fig.show()
57% пользователей не перешли на другой уровень и играли в среднем около 9 дней.
29% пользователей выбрали боевую стратегию и среднее время прохождения этого уровня составило почти 11 дней.
И почти 13 дней потребовалось пользователям, которые продолжали строительство объектов.
В день в игру заходят 2884 уникальных пользователя, которые делают за это время 4110 действий. Рост пользователей и сессий наблюдался с 4 по 10 мая 2020 года, затем эти значения снижались.
С 4 мая по 5 июня 2020 года 13576 пользователей успели построить 127957 объектов. 5817 пользователей закончили первый уровень, из них 1866 завершили проект.
При выборе стратегии строительства пользователи строят объекты в следующем порядке: spaceport, assembly_shop, research_center. За указанное время объект spaceport был построен наибольшее количество раз (59325), assembly_shop был построен 54494 раз, а research_center построили только 14138 раз.
В среднем каждый пользователь провёл 10 дней в игре. Для завершения уровня потребуется в среднем от 11 (боевая стратегия) до 13 дней (стратегия строительства). Пользователи, которые не перешли на следующий уровень, (57% от общего числа) провели в игре 9 дней.
ad_costs
all_costs = ad_costs['cost'].sum()
all_costs
costs_for_source = ad_costs.groupby('source').agg({'cost': 'sum'}).rename(columns={'cost':'total_costs'}).reset_index()
costs_for_source
costs_for_source.plot(kind='bar', x='source', y='total_costs', color = '#a52a2a',
figsize=(16,8), legend=True, title='Затраты на маркетинг по ресурсам');
Наибольшее количество средств было потрачено на привлечение пользователей через yandex_direct (2233). В 2 раза меньше средств было потрачено на youtube_channel_reklama (1068).
day_costs = ad_costs.groupby('day')['cost'].sum()
day_costs.plot(kind='bar', x='day', y='cost', color = '#a52a2a',
figsize=(16,8), legend=True, title='Затраты на маркетинг по дням');
Больше всего вложений было в первый день, 3 мая 2020. С каждым днём затраты на маркетинг значительно снижались.
user_source
users_sources = user_source.groupby('source').agg({'user_id':'nunique'}).rename(columns={'user_id':'users'})
users_sources
table_source = users_sources.merge(costs_for_source, on='source', how='outer')
table_source
table_source['cac'] = table_source['total_costs'] / table_source['users']
table_source
table_source.plot(kind='bar', x='source', y='cac', color = '#a52a2a',
figsize=(16,8), legend=True, title='Стоимость привлечения 1 пользователя по источникам');
Стоимость привлечения пользователя из facebook_ads выше, чем стоимость привлечения пользователей через другие ресурсы. Самым выгодным вложением стала реклама в youtube_channel_reklama, затраты на этот ресурс были самыми низкими, а пользователей в игру пришло много.
Посмотрим на этот показатель в динамике по источникам:
first_event = game_actions.groupby('user_id')['event_datetime'].min().reset_index()
first_event.columns = ['user_id', 'first_event_date']
first_event
first_event['day'] = first_event['first_event_date'].astype('datetime64[D]')
source_users = user_source.merge(first_event, on='user_id', how='inner')
source_users
source_users_costs = source_users.merge(ad_costs, on=['source', 'day'], how='outer')
source_users_costs
cost_per_user_in_day = source_users_costs.groupby(['source', 'day']).agg({'user_id':'nunique', 'cost':'max'})
cost_per_user_in_day['cac'] = cost_per_user_in_day['cost'] / cost_per_user_in_day['user_id']
cost_per_user_in_day
fig = go.Figure()
fig.add_trace(go.Bar(
x=cost_per_user_in_day.loc['facebook_ads'].index,
y=cost_per_user_in_day.loc['facebook_ads']['cac'],
name='facebook_ads',
marker_color='indianred'
))
fig.add_trace(go.Bar(
x=cost_per_user_in_day.loc['instagram_new_adverts'].index,
y=cost_per_user_in_day.loc['instagram_new_adverts']['cac'],
name='instagram_new_adverts',
marker_color='lightsalmon'
))
fig.add_trace(go.Bar(
x=cost_per_user_in_day.loc['yandex_direct'].index,
y=cost_per_user_in_day.loc['yandex_direct']['cac'],
name='yandex_direct',
marker_color='maroon'
))
fig.add_trace(go.Bar(
x=cost_per_user_in_day.loc['youtube_channel_reklama'].index,
y=cost_per_user_in_day.loc['youtube_channel_reklama']['cac'],
name='youtube_channel_reklama',
marker_color='rebeccapurple'
))
fig.update_layout(
barmode='group',
title='Затраты на привлечение 1 пользователя по источникам в динамике',
title_x = 0.5,
margin=dict(l=50, r=50, t=130, b=50))
fig.show()
Стоимость привлечения 1 пользователя через youtube_channel_reklama практически во все дни рекламной компании была самой низкой. Дороже всего обходился пользователь из facebook_ads.
Будем использовать известный принцим для расчета оптимального соотношения LTV и CAC:
1:1 или меньше — бизнес обречен на провал, если срочно не улучшить ситуацию;
2:1 — затраты на привлечение клиентов практически не окупаются;
3:1 — бизнес-модель работает продуктивно: именно такое соотношение можно назвать оптимальным, к нему нужно стремиться;
4:1 — ваш бизнес очень продуктивный — клиенты стоят недорого и приносят компании хорошую прибыль.
table_source
table_source['good_revenue'] = table_source['cac'] * 3
table_source
table_source['price_per_ad'] = table_source['good_revenue']/2
table_source
На привлечение пользователей с 3 по 9 мая 2020 года потратили 7603. Наибольшая сумма затрат была у источника yandex_direct (2233), а меньше всего средств потратили на привлечение через youtube_channel_reklama (1068).
Значительная сумма была потрачена в первый день компании, затем расходы постепенно снижались.
Больше всего пользователей пришло из yandex_direct (4817), а дешевле всего обошёлся пользователь из youtube_channel_reklama.
Для того, чтобы покрыть затраты на привлечение, нужно встраивать рекламу, которая будет приносить в 3 раза больше, чем мы потратили на привлечение пользователя. В каждом из источников стоит выставить свои ограничения.
Нулевая — пользователи, которые выбирают боевую стратегию, тратят столько же времени, сколько и те, кто строит спутники. Альтернативная — затраченное время у пользователей, выбравших разные стратегии, отличается.
alpha = 0.05
results = st.ttest_ind(users_with_build_strategy['duration_days'], users_with_fight_strategy['duration_days'], equal_var = False)
print('p-значение: ', results.pvalue)
if (results.pvalue < alpha):
print("Отвергаем нулевую гипотезу, различие между пользователями есть")
else:
print("Не получилось отвергнуть нулевую гипотезу, различия между пользователями нет")
Затраченное время у пользователей, выбравших разные стратегии, отличается. По нашим расчётам пользователям, выбравшим боевую стратегию требуется в среднем 11 дней. А пользователям, которые завершили уровень с помощью постройки объектов, тратят на прохождение уровня 13 дней.
Нулевая — пользователи, привлеченные из одного рекламного источника, тратят столько же времени для прохождения уровня, сколько и пользователи, привлеченные из другого источника.
Альтернативная — время, которое для завершения уровня потратили пользователи, привлеченные из одного рекламного источника, отличается от времени, которое потратили пользователи из другого рекламного источника.
duration_using = duration_using.merge(user_source, on='user_id', how='left')
duration_using
facebook = duration_using[['source', 'duration_days']].query('source == "facebook_ads"')
instagram = duration_using[['source', 'duration_days']].query('source == "instagram_new_adverts"')
yandex_direct = duration_using[['source', 'duration_days']].query('source == "yandex_direct"')
youtube_channel_reklama = duration_using[['source', 'duration_days']].query('source == "youtube_channel_reklama"')
Чтобы снизить групповую вероятность ошибки первого рода, можно использовать метод Шидака (он повысит мощность теста) или Бенферони. Будем использовать метод Шидака:
def st_test (group_1, group_2, alpha):
results = st.ttest_ind(group_1['duration_days'], group_2['duration_days'], equal_var = False)
print('p-значение: ', results.pvalue)
if (results.pvalue < alpha):
print("Отвергаем нулевую гипотезу, различие между пользователями есть")
else:
print("Не получилось отвергнуть нулевую гипотезу, различия между пользователями нет")
shidak_alpha = [1 - (1 - alpha)**(1/m) for m in range(1, 7)]
shidak_alpha
st_test(facebook, instagram, shidak_alpha[0])
st_test(facebook, yandex_direct, shidak_alpha[1])
st_test(facebook, youtube_channel_reklama, shidak_alpha[2])
st_test(instagram, yandex_direct, shidak_alpha[3])
st_test(instagram, youtube_channel_reklama, shidak_alpha[4])
st_test(yandex_direct, youtube_channel_reklama, shidak_alpha[5])
Гипотезу отвергнуть не получилось, статистически значимых различий во времени прохождения уровня между пользователями из разных источников трафика нет.
Время прохождения уровня зависит от выбранной стратегии, но не зависит от рекламного источника, через который пришёл пользователь.
Большая часть пользователей (57%) не заканчивают уровень и тратят на игру в среднем 9 дней. Возможно, стоит упростить какие-то этапы, чтобы пользователи не отваливались так рано.
Время прохождения уровня отличается у пользователей, выбравших разные стратегии. Быстрее всего удаётся пройти уровень пользователям с боевой стратегией. 29% пользователей выбрали боевую стратегию. Возможно, вариант строительства объектов слишком затянут и стоит перепридумать ход игры.
Что касается монетизации, то рекламу точно стоит показывать до завершения уровня, ведь к этому этапу не доходят 57% пользователей.
Выбор стоимости показа рекламы стоит осуществлять исходя из cac. Для того чтобы получить прибыль, стоит закладывать стоимость показа в 3 раза выше, чем стоимость привлечения одного пользователя. Практически все пользователи строят первые 2 объекта, поэтому можно показывать рекламу перед выбором постройки стоимостью в 1,5 раза выше, чем стоимость привлечения пользователя.